package com.tangcheng.adapter.db.mybatis.plugin.sensitive;

import com.alibaba.fastjson.JSON;
import com.tangcheng.adapter.db.mybatis.plugin.sensitive.annotation.SensitiveField;
import com.tangcheng.adapter.db.mybatis.plugin.sensitive.annotation.SensitiveFieldEncryption;
import com.tangcheng.adapter.db.mybatis.plugin.sensitive.constant.EncryptionTypeEnum;
import com.tangcheng.adapter.db.mybatis.plugin.sensitive.processor.DecryptSensitiveField;
import com.tangcheng.adapter.db.mybatis.plugin.sensitive.processor.EncryptSensitiveField;
import com.tangcheng.adapter.db.mybatis.plugin.sensitive.service.IEncryptionAlgorithm;
import com.tangcheng.adapter.db.user.dao.entity.SysAccountExample;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;


@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class SensitiveFieldInterceptor implements Interceptor {

    private IEncryptionAlgorithm encryptionAlgorithm;
    /**
     * 全局AES_SECRET密钥 key,可直接在mapper.xml文件使用
     * 如果有多个插件时会有冲突导致不可用
     */
    private static final String AES_SECRET_KEY = "aesSecret";
    private Integer STATEMENT_INDEX = 0;
    private Integer PARAMETER_INDEX = 1;

    // 有需要加解密的加密的Example和字段
    private static final Map<Class<?>, List<String>> ENCRY_EXAMPLE_MAP = new HashMap<Class<?>, List<String>>() {{
        put(SysAccountExample.class, Collections.singletonList("email"));
    }};

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取当前方法
        Method targetMethod = getCurrentMethod(invocation);
        log.debug(" targetMethod {} ", targetMethod);

        boolean needEncrypt = false;
        boolean needDecrypt = false;
        //判断方法上是否存在加解密注解
        if (targetMethod != null && targetMethod.isAnnotationPresent(SensitiveFieldEncryption.class)) {
            log.debug("加解密处理");
            SensitiveFieldEncryption sensitiveFieldEncryption = targetMethod.getAnnotation(SensitiveFieldEncryption.class);
            //判断是否需要解密
            if (EncryptionTypeEnum.DECRYPT == sensitiveFieldEncryption.value()) {
                log.debug("需要解密处理");
                needDecrypt = true;
            } else if (EncryptionTypeEnum.ENCRYPT == sensitiveFieldEncryption.value()) {
                log.debug("需要加密处理");
                needEncrypt = true;
            } else {
                log.debug("需要加解密处理");
                needDecrypt = true;
                needEncrypt = true;
            }
        }
        //需要加密
        if (needEncrypt) {
            handleEncrypt(invocation, targetMethod, true);
        } else if (needDecrypt) {
            //放入盐值
            handleEncrypt(invocation, targetMethod, false);
        }
        Object resultObject = invocation.proceed();
        //对已加密的入参，进行解密，防止影响入参后续使用
        if (needEncrypt) {
            handleEncrypt(invocation, targetMethod, false);
        }
        //需要解密,且响应结果不为null
        if (needDecrypt && !Objects.isNull(resultObject)) {
            resultObject = (new DecryptSensitiveField(encryptionAlgorithm)).handle(resultObject);
        }
        return resultObject;
    }

    public SensitiveFieldInterceptor() {
        log.info(" SensitiveFieldInterceptor init ");
    }

    /**
     * 获取当前方法
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    private Method getCurrentMethod(Invocation invocation) throws Throwable {
        MappedStatement statement = (MappedStatement) invocation.getArgs()[STATEMENT_INDEX];
        String classMethodPath = statement.getId();
        log.debug(" id {} ", classMethodPath);
        String[] s = StringUtils.split(classMethodPath, ".");
        String methodName = s[s.length - 1];
        String classPath = classMethodPath.replace("." + methodName, "");
        //反射获取对应mapper接口类和方法
        Class targetClass = Class.forName(classPath);
        for (Method method : targetClass.getDeclaredMethods()) {
            if (method.getName().equalsIgnoreCase(methodName)) {
                return method;
            }
        }
        log.warn(" 方法未找到 id {} getDeclaredMethods {} ", classMethodPath, JSON.toJSONString(targetClass.getDeclaredMethods()));
        return null;
    }

    /**
     * 处理加密
     *
     * @param invocation
     * @param targetMethod
     */
    private void handleEncrypt(Invocation invocation, Method targetMethod, boolean isEncrypt) throws Throwable {
        List<String> markedFieldNames = new ArrayList<>();
        for (int i = 0; i < targetMethod.getParameters().length; i++) {
            Parameter p = targetMethod.getParameters()[i];
            if (p.isAnnotationPresent(SensitiveField.class)) {
                if (p.isAnnotationPresent(Param.class)) {
                    //@Param注解的名称
                    markedFieldNames.add(p.getAnnotation(Param.class).value());
                } else {
                    //默认参数名称
                    markedFieldNames.add("param" + (i + 1));
                }
            }
        }
        log.debug("markedFieldNames : {}", StringUtils.join(markedFieldNames, ","));
        Object parameter = invocation.getArgs()[PARAMETER_INDEX];
        Object processSingle = processSingle(parameter, markedFieldNames, isEncrypt);
        addAesSecret(processSingle);
        invocation.getArgs()[PARAMETER_INDEX] = processSingle;
    }

    private void addAesSecret(Object invocationNewParameter) {
        if (invocationNewParameter == null) {
            return;
        }
        if (invocationNewParameter instanceof Map) {
            ((Map<String, Object>) invocationNewParameter).put(AES_SECRET_KEY, encryptionAlgorithm.getAesSecret());
        }
    }

    private Object processSingle(Object paramObj, List<String> sensitiveFieldNames, boolean isEncrypt) throws Exception {
        if (CollectionUtils.isEmpty(sensitiveFieldNames)) {
            return paramObj;
        }
        Map<String, Object> paramMap = new MapperMethod.ParamMap<>();
        //无参数处理
        if (paramObj == null) {
            paramObj = paramMap;
            // 单参数 将 参数转为 map
        } else if (ClassUtils.isPrimitiveOrWrapper(paramObj.getClass())
                || String.class.isAssignableFrom(paramObj.getClass())) {
            if (sensitiveFieldNames.size() == 1) {
                paramMap.put(sensitiveFieldNames.iterator().next(), handle(paramObj, isEncrypt));
                paramObj = paramMap;
            }
        } else {
            //多参数或对象处理
            processParam(paramObj, sensitiveFieldNames, isEncrypt);
        }
        return paramObj;
    }

    /**
     * 处理多参数或对象参数
     *
     * @param parameterObject
     * @param sensitiveFieldNames
     * @param isEncrypt           是否加密,true
     */
    private void processParam(Object parameterObject, List<String> sensitiveFieldNames, boolean isEncrypt) {
        if (parameterObject instanceof Map) {
            // 处理参数对象  如果是 map 且map的key 中没有 tenantId，添加到参数map中
            if (CollectionUtils.isNotEmpty(sensitiveFieldNames)) {
                Map<String, Object> changeField = new HashMap<>();
                Map<String, Object> mapParameter = (Map) parameterObject;
                mapParameter.forEach((k, v) -> {
                    log.debug("多参数 k: {} v: {}", k, v);
                    if (sensitiveFieldNames.contains(k)) {
                        changeField.put(k, handle(v, isEncrypt));
                    }
                });
                for (Map.Entry<String, Object> entry : changeField.entrySet()) {
                    mapParameter.put(entry.getKey(), entry.getValue());
                }
            }
        } else {
            // 如果参数是bean，反射设置值
            handle(parameterObject, isEncrypt);
        }
    }

    private Object handle(Object parameter, boolean isEncrypt) {
        if (parameter == null) {
            return null;
        }
        try {
            List<String> sensitiveFieldListMarked = ENCRY_EXAMPLE_MAP.get(parameter.getClass());
            if (CollectionUtils.isNotEmpty(sensitiveFieldListMarked)) {
                Field oredCriteria = parameter.getClass().getDeclaredField("oredCriteria");
                oredCriteria.setAccessible(true);
                Object oredCriteriaVal = oredCriteria.get(parameter);
                oredCriteria.setAccessible(false);
                List criList = (List) oredCriteriaVal;
                for (Object cri : criList) {
                    Field criteriaField = cri.getClass().getSuperclass().getDeclaredField("criteria");
                    criteriaField.setAccessible(true);
                    Object criteriaObj = criteriaField.get(cri);
                    criteriaField.setAccessible(false);
                    List criterionList = (List) criteriaObj;
                    for (Object criterion : criterionList) {
                        Field conditionField = criterion.getClass().getDeclaredField("condition");
                        conditionField.setAccessible(true);
                        Object conditionVal = conditionField.get(criterion);
                        conditionField.setAccessible(false);
                        if (isSensitiveFieldList(sensitiveFieldListMarked, conditionVal.toString())) {
                            Field valueField = criterion.getClass().getDeclaredField("value");
                            valueField.setAccessible(true);
                            Object currentValueVal = valueField.get(criterion);
                            if (isEncrypt) {
                                valueField.set(criterion, (new EncryptSensitiveField(encryptionAlgorithm)).handleObject(currentValueVal));
                            } else {
                                valueField.set(criterion, (new DecryptSensitiveField(encryptionAlgorithm)).handle(currentValueVal));
                            }
                            valueField.setAccessible(false);
                        }
                    }
                }
                return parameter;
            }
        } catch (Exception e) {
            log.error("处理Example数据 parameter {} {}", parameter, e.getMessage(), e);
            return parameter;
        }

        if (isEncrypt) {
            //加密处理
            return (new EncryptSensitiveField(encryptionAlgorithm)).handleObject(parameter);
        } else {
            return (new DecryptSensitiveField(encryptionAlgorithm)).handle(parameter);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    public void setEncryptionAlgorithm(IEncryptionAlgorithm encryptionAlgorithm) {
        this.encryptionAlgorithm = encryptionAlgorithm;
    }

    public static boolean isSensitiveFieldList(List<String> sensitiveFieldsMarked, String conditionVal) {
        if (CollectionUtils.isEmpty(sensitiveFieldsMarked) || StringUtils.isEmpty(conditionVal)) {
            return false;
        }
        String filedNameInCondition = conditionVal.trim().split(" ")[0];
        for (String sensitiveField : sensitiveFieldsMarked) {
            if (Objects.equals(filedNameInCondition.trim(), sensitiveField.trim())) {
                return true;
            }
        }
        return false;
    }

}